package run.bottle.app.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import run.bottle.app.exception.AuthenticationException;
import run.bottle.app.model.entity.SysUser;
import run.bottle.app.security.authentication.AuthenticationImpl;
import run.bottle.app.security.context.SecurityContextHolder;
import run.bottle.app.security.context.SecurityContextImpl;
import run.bottle.app.security.handler.AuthenticationFailureHandler;
import run.bottle.app.security.handler.DefaultAuthenticationFailureHandler;
import run.bottle.app.security.support.UserDetail;
import run.bottle.app.security.util.SecurityUtils;
import run.bottle.app.service.UserService;
import run.bottle.app.utils.RedisUtil;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

import static run.bottle.app.security.support.BottleConst.ADMIN_TOKEN_HEADER_NAME;
import static run.bottle.app.security.support.BottleConst.ADMIN_TOKEN_QUERY_NAME;

/**
 * Api authentication Filter
 *
 * @author Lycheng
 */
@Slf4j
@Component
@Order(0)
public class AuthenticationFilter extends AbstractAuthenticationFilter  {

    private final UserService userService;

    public AuthenticationFilter(UserService userService, ObjectMapper objectMapper, RedisUtil cacheStore) {
        super(cacheStore);
        this.userService = userService;

        addUrlPatterns("/api/**");

        addExcludeUrlPatterns(
                "/api/login",
                "/hello"
        );


        // set failure handler
        DefaultAuthenticationFailureHandler failureHandler = new DefaultAuthenticationFailureHandler();
        failureHandler.setProductionEnv(true);
        failureHandler.setObjectMapper(objectMapper);

        setFailureHandler(failureHandler);
    }

    @Override
    protected void doAuthenticate(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // Get token from request
        String token = getTokenFromRequest(request);

        if (StringUtils.isEmpty(token)) {
            throw new AuthenticationException("未登录，请登录后访问");
        }

        // Get user id from cache
        Object optionalUserId = cacheStore.get(SecurityUtils.buildTokenAccessKey(token));

        if (StringUtils.isEmpty(optionalUserId)) {
            throw new AuthenticationException("Token 已过期或不存在").setErrorData(token);
        }

        // Get the user
        SysUser user = userService.getById((Integer) optionalUserId);
        // Build user detail
        UserDetail userDetail = new UserDetail(user);

        // Set security
        SecurityContextHolder.setContext(new SecurityContextImpl(new AuthenticationImpl(userDetail)));

        // Do filter
        filterChain.doFilter(request, response);
    }

    /**
     * Sets authentication failure handler.
     *
     * @param failureHandler authentication failure handler
     */
    public synchronized void setFailureHandler(@NonNull AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "Authentication failure handler must not be null");
        this.failureHandler = failureHandler;
    }

    /**
     * Adds exclude url patterns.
     *
     * @param excludeUrlPatterns exclude urls
     */
    public void addExcludeUrlPatterns(@NonNull String... excludeUrlPatterns) {
        Assert.notNull(excludeUrlPatterns, "Exclude url patterns must not be null");

        Collections.addAll(this.excludeUrlPatterns, excludeUrlPatterns);
    }

    public void addUrlPatterns(String... urlPatterns) {
        Assert.notNull(urlPatterns, "UrlPatterns must not be null");
        Collections.addAll(this.urlPatterns, urlPatterns);
    }

    @Override
    protected String getTokenFromRequest(@NonNull HttpServletRequest request) {
        return getTokenFromRequest(request, ADMIN_TOKEN_QUERY_NAME, ADMIN_TOKEN_HEADER_NAME);
    }
}
